package org.dreamwork.assistant.tools.cli;

import org.dreamwork.cli.ArgumentParser;
import org.dreamwork.util.IOUtil;
import org.dreamwork.util.StringUtil;

import java.io.*;
import java.nio.file.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * Created by seth.yang on 2017/7/24<br/>
 * {"shortOption":"r", "longOption":"repo","description":"the maven repo root", "required":true, "requireValue":true},<br/>
 * {"shortOption":"m", "longOption":"main-class","description":"the main java class", "required":true, "requireValue":true},<br/>
 * {"shortOption":"j", "longOption":"java-home","description":"the java home", "requireValue":true},<br/>
 * {"shortOption":"o", "longOption":"output","description":"the output dir","defaultValue":"out", "requireValue":true},<br/>
 * {"shortOption":"p", "longOption":"pom","description":"the maven pom file","defaultValue":"pom.xml", "requireValue":true},<br/>
 * {"shortOption":"f", "longOption":"force-include","description":"force include all dependencies"},<br/>
 * {"shortOption":"i", "longOption":"include", "requireValue":true,"description":"include the all jar files under this dir"},<br/>
 * {"shortOption":"R", "longOption":"recursive", "description":"recursive the include dir if -i option present."},<br/>
 *<br/>
 * {"shortOption":"c", "longOption":"compile", "description":"include compile scope libs"},<br/>
 * {"shortOption":"C", "longOption":"with-config","requireValue":true,"description":"the config file with be copy to distribute dir"},<br/>
 * {"longOption":"gen-debug-script","description":"generate a debug version of the script"},<br/>
 * {"longOption":"debug-port","description":"JVM debug port","requireValue":true,"defaultValue":"5005"},<br/>
 * {"longOption":"with-win-script","description":"generate a windows batch script"},<br/>
 * {"longOption":"with-linux-script", "description":"generate a linux bash script"}<br/>
 *
 * <pre>
 * Distributor -p &lt;path-to-pom-file&gt;   \
 *             -j &lt;path-to-java-home&gt;  \
 *             -r &lt;path-to-maven-repo&gt; \
 *             -m &lt;main-class-name&gt;    \
 *            [-o &lt;out-dir&gt;]           \
 *            [-f --force-include]     \
 *            [-i &lt;include-dir&gt;]       \
 *            [-R --recursive]         \
 *            [-c --compile]           \
 *            [-C &lt;path-to-config&gt;]    \
 *            [--gen-debug-script]     \
 *            [--debug-port=&lt;port&gt;]    \
 *            [--with-win-script]      \
 *            [--with-linux-script]
 * </pre>
 */
public class Distributor {
    private static String repo, name;
    private static boolean force, recursive, compile;
    private static File libs;
    private static final char[] letter = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    public static void main (String[] args) throws Exception {
        byte[] buff;
        try (InputStream in = Distributor.class.getClassLoader ().getResourceAsStream ("distributor.json")) {
            buff = IOUtil.read (in);
        }
        if (buff == null) {
            return;
        }

        String json = new String (buff, "utf-8");
        ArgumentParser parser = new ArgumentParser (json);
        parser.parse (args);

        repo      = new File (parser.getValue ('r')).getCanonicalPath ();
        force     = parser.isArgPresent ('f');
        recursive = parser.isArgPresent ('R');
        compile   = parser.isArgPresent ('c');
        name      = parser.getValue ('n');

        String mainClass    = parser.getValue ('m');
        String javaHome     = parser.getValue ('j');
        String outDir       = parser.getValue ('o');
        String pom          = parser.getValue ('p');
        String incDir       = parser.getValue ('i');
        String configFile   = parser.getValue ('C');
        String s_debug_port = parser.getValue ("debug-port");

        int debug_port      = -1;
        boolean win_script  = parser.isArgPresent ("with-win-script");
        boolean lin_script  = parser.isArgPresent ("with-linux-script");
        boolean gen_debug   = parser.isArgPresent ("gen-debug-script");
        if (gen_debug) {
            if (StringUtil.isEmpty (s_debug_port)) {
                s_debug_port = parser.getDefaultValue ("debug-port");
            }

            debug_port      = Integer.parseInt (s_debug_port);
        }

        if (StringUtil.isEmpty (pom)) {
            pom = parser.getDefaultValue ('p');
        }

        File pomFile = new File (pom);
        if (!pomFile.exists ()) {
            System.err.println ("can't find pom file: " + pom);

            System.exit (-1);
        }

        if (!StringUtil.isEmpty (pom)) {
/*
            try (OutputStream out = new FileOutputStream (new File (bin, "run.bat"))) {
                shell = new PrintWriter (out, true);
                shell.println ("@echo off");
                shell.println ();
                if (!StringUtil.isEmpty (javaHome))
                    shell.printf ("SET JAVA_HOME=%s%n", javaHome);
                shell.printf ("SET MAIN_CLASS=%s%n", mainClass);

                Set<String> sources = new HashSet<> ();
                process2 (pom, sources, 0);
                if (!StringUtil.isEmpty (incDir)) {
                    File dir = new File (incDir);
                    if (dir.exists () && dir.isDirectory ()) {
                        processInclude (new File (incDir), sources);
                    }
                }

                shell.println ();
                shell.printf ("%%JAVA_HOME%%\\bin\\java -cp %%CP%% %%MAIN_CLASS%% %%*");
                out.flush ();

                System.out.println ("***************************");
                for (String target: sources) {
                    System.out.println (target);
                }
                System.out.println ("***************************");
            }
*/
            Set<String> sources = new HashSet<> ();
            process2 (pom, sources, 0);
            if (!StringUtil.isEmpty (incDir)) {
                File dir = new File (incDir);
                if (dir.exists () && dir.isDirectory ()) {
                    processInclude (new File (incDir), sources);
                }
            }

            // prepare the base dir
            File basedir;
            if (StringUtil.isEmpty (outDir)) {
                outDir = parser.getDefaultValue ('o') + '/' + name;
            }
            outDir = outDir.trim ();
            if (outDir.startsWith ("file://") || outDir.charAt (0) == '/') {
                basedir = new File (outDir);
            } else {
                basedir = new File (".", outDir);
            }

            if (!basedir.exists () && !basedir.mkdirs ()) {
                throw new IOException ("can't create basedir: " + basedir.getCanonicalPath ());
            }
            File bin = new File (basedir, "bin");
            if (!bin.exists () && !bin.mkdirs ()) {
                throw new IOException ("can't create bin dir: " + bin.getCanonicalPath ());
            }
            libs = new File (basedir, "libs");
            if (!libs.exists () && !libs.mkdirs ()) {
                throw new IOException ("can't create libs dir:" + libs.getCanonicalPath ());
            }
            File conf = new File (basedir, "conf");
            if (!conf.exists () && !conf.mkdirs ()) {
                throw new IOException ("can't create conf dir: " + conf.getCanonicalPath ());
            }
            File logs = new File (basedir, "logs");
            if (!logs.exists () && !logs.mkdirs ()) {
                throw new IOException ("can't create logs dir: " + logs.getCanonicalPath ());
            }

            if (!StringUtil.isEmpty (configFile)) {
                File src;
                configFile = configFile.trim ();
                if (configFile.startsWith ("/") || configFile.startsWith ("file://")) {
                    src = new File (configFile);
                } else {
                    src = new File (pomFile.getParent (), configFile);
                }

                if (!src.exists ()) {
                    System.err.println ("can't open config file: " + src.getCanonicalPath ());
                    System.exit (-2);
                }
                String fileName = src.getName ();
                Path target = Paths.get (basedir.getCanonicalPath (), "conf", fileName);
                System.out.println ("    - copying " + src.getCanonicalPath () + " to " + target);
                Files.copy (src.toPath (), target, REPLACE_EXISTING);
            }

            // copying the jars
            if (!sources.isEmpty ()) {
                for (String path : sources) {
                    Path p = Paths.get (path);
                    String fileName = p.toFile ().getName ();
                    if (Files.exists (p, LinkOption.NOFOLLOW_LINKS)) {
                        Path target = Paths.get (libs.getPath () + '/' + fileName);
                        Files.copy (p, target, REPLACE_EXISTING);
                        System.out.println ("    - copying " + path + " to " + target);
                    }
                }
            }

            if (win_script || lin_script) {
                if (StringUtil.isEmpty (name)) {
                    name = "startup";
                }

                Script win = null, linux = null, win_debug = null, lin_debug = null;
                if (win_script) {
                    File target = new File (bin, name + ".bat");
                    win = new Script.WinScript (target, false);

                    if (gen_debug) {
                        target = new File (bin, name + "-debug.bat");
                        win_debug = new Script.WinScript (target, true);
                    }
                }

                if (lin_script) {
                    File target = new File (bin, name + ".sh");
                    linux = new Script.LinuxScript (target, false);

                    if (gen_debug) {
                        target = new File (bin, name + "-debug.sh");
                        lin_debug = new Script.LinuxScript (target, true);
                    }
                }

                Script.Env env  = new Script.Env ();
                env.mainClass   = mainClass;
                env.javaHome    = javaHome;
                env.outDir      = outDir;
                env.configFile  = configFile;
                env.debug_port  = debug_port;
                env.base        = basedir;
                if (win != null) try {
                    win.generate (env);
                } finally {
                    win.close ();
                }

                if (linux != null) try {
                    linux.generate (env);
                } finally {
                    linux.close ();
                }

                if (win_debug != null) try {
                    win_debug.generate (env);
                } finally {
                    win_debug.close ();
                }

                if (lin_debug != null) try {
                    lin_debug.generate (env);
                } finally {
                    lin_debug.close ();
                }
            }

            generateREADME (basedir, mainClass, pom);
        }
    }

    private static void generateREADME (File basedir, String mainClass, String pom) throws IOException {
        // generate a README file
        File readme = new File (basedir, "README");
        try (FileWriter writer = new FileWriter (readme)) {
            final String CRLF = "\r\n";
            SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss");
            writer.write ("Distributed by dreamwork java distributor v2.1.0 at");
            writer.write (format.format (System.currentTimeMillis ()));
            writer.write (CRLF);
            writer.write ("You can find the last version at ");
            writer.write (CRLF);
            writer.write ("https://gitee.com/seth_yang/dreamwork/tree/master/modules/assistant");
            writer.write (CRLF);
            writer.write (CRLF);
            writer.flush ();

            File project = new File (pom);
            final File parent = new File (project.getParentFile (), "target");
            File[] jars = parent.listFiles (new FileFilter () {
                @Override
                public boolean accept (File pathname) {
                    return pathname.getName ().endsWith (".jar");
                }
            });
            if (jars != null && jars.length > 0) {
                JarFile jar = new JarFile (jars [0]);
                InputStream in = null;

                for (Enumeration<JarEntry> en = jar.entries (); en.hasMoreElements ();) {
                    JarEntry e = en.nextElement ();
                    if (e.getName ().endsWith (".json")) {
                        in = jar.getInputStream (e);
                        break;
                    }
                }

                if (in != null) try {
                    String json = new String (IOUtil.read (in), "utf-8");
                    ArgumentParser parser = new ArgumentParser (json);
                    writer.write ("The command line arguments of ");
                    writer.write (mainClass);
                    writer.write (" show as followings: ");
                    writer.write (CRLF);
                    parser.showHelp (new PrintWriter (writer, true));
                } finally {
                    in.close ();
                }
            }
        }
    }

    private static final FileFilter filter = new FileFilter () {
        @Override
        public boolean accept (File file) {
            if (file.isDirectory ()) return true;
            String name = file.getName ();
            return name.endsWith (".jar") || name.endsWith (".zip");
        }
    };

    private static void processInclude (File dir, Set<String> sources) throws IOException {
        File[] jars = dir.listFiles (filter);
        if (jars != null) {
            for (File file : jars) {
                if (file.isDirectory () && recursive) {
                    processInclude (file, sources);
                } else {
                    String name = file.getName ();
                    Path target = Paths.get (libs.getCanonicalPath (), name);
                    if (Files.exists (target, LinkOption.NOFOLLOW_LINKS))
                        sources.add (target.toString ());
                }
            }
        }
    }

    private static final Pattern PATTERN = Pattern.compile ("^\\$\\{(.*?)}");
    private static String replace (XMLReader reader, String value) {
        if (reader == null || StringUtil.isEmpty (value))
            return value;

        if (!value.contains ("${")) {
            return value;
        }

        Matcher m = PATTERN.matcher (value);
        if (m.matches ()) {
            value = m.group (1);
            if (value.startsWith ("project.")) {
                value = value.substring ("project.".length ());
                Object o = reader.getValue (value);
                if (o == null) {
                    o = reader.getValue ("parent/" + value);
                }
                if (o != null) {
                    if (o.getClass () == String.class) {
                        return (String) o;
                    } else {
                        System.out.println (o);
                    }
                }
            } else {
                return (String) reader.getValue ("properties/" + value);
            }
        }
        return value;
    }

    @SuppressWarnings ("unchecked")
    private static void process2 (String pom, Set<String> sources, int level) throws Exception {
        if (level == 0) {
            File project = new File (pom);
            final File parent = project.getParentFile ();
            File target = new File (parent, "target");
            File[] files = target.listFiles (new FileFilter () {
                @Override
                public boolean accept (File pathname) {
                    return pathname.getName ().endsWith (".jar");
                }
            });

            if (files != null && files.length > 0) {
                for (File jar : files) {
//                    Path p = Paths.get (libs.getCanonicalPath (), jar.getName ());
                    sources.add (jar.getCanonicalPath ());
                }
            }
        }

        XMLReader reader;
        try (InputStream in = new FileInputStream (pom)) {
            reader = new XMLReader (in);
        }

        if (level == 0 && StringUtil.isEmpty (name)) {
            name = (String) reader.getValue ("artifactId");
            if (StringUtil.isEmpty (name)) {
                name = (String) reader.getValue ("parent/artifactId");
            }
            if (!StringUtil.isEmpty (name)) {
                char[] tmp = name.toCharArray ();
                StringBuilder builder = new StringBuilder ();
                for (char ch : tmp) {
                    if (!Character.isJavaIdentifierPart (ch) && ch != '-' && ch != '_') {
                        builder.append ("_$");
                        builder.append (letter [(ch >> 4) & 0x0f]);
                        builder.append (letter [(ch) & 0x0f]);
                        builder.append ('_');
                    } else {
                        builder.append (ch);
                    }
                }

                name = builder.toString ();
            }
        }

        List<XMLReader.TreeNode> deps = (List<XMLReader.TreeNode>) reader.getValue ("dependencies");
        if (deps != null) {
            for (XMLReader.TreeNode node : deps) {
                String path = "";
                String groupId = (String) reader.getValue (node, "groupId");
                String artifactId = (String) reader.getValue (node, "artifactId");
                String version = (String) reader.getValue (node, "version");
                String scope = (String) reader.getValue (node, "scope");

                boolean use = force ||
                        StringUtil.isEmpty (scope) ||
                        (compile && "compile".equals (scope));

                if (!use) {
                    continue;
                }

                if (!StringUtil.isEmpty (groupId)) {
                    groupId = replace (reader, groupId);
                    path += groupId.replace ('.', '/');
                }

                if (!StringUtil.isEmpty (artifactId)) {
                    artifactId = replace (reader, artifactId);
                    path += '/' + artifactId;
                }

                if (StringUtil.isEmpty (version)) {
                    File p = new File (repo, path);
                    File[] children = p.listFiles (new FileFilter () {
                        @Override
                        public boolean accept (File pathname) {
                            return pathname.isDirectory ();
                        }
                    });
                    if (children != null) {
                        Version max = new Version (null);
                        for (File file : children) {
                            Version v = new Version (file.getName ());
                            if (max.compareTo (v) < 0) {
                                max = v;
                            }
                        }
                        version = max.version;
                    }

                    if (StringUtil.isEmpty (version)) {
                        continue;
                    }
                }
                version = replace (reader, version);

                path += '/' + version;
                String fileName = artifactId + '-' + version + ".jar";
                File file = new File (repo, path + '/' + fileName);
                if (file.exists ()) {
                    String dep = file.getCanonicalPath ();
                    if (!sources.contains (dep)) {
                        sources.add (dep);
                        File p = new File (repo, path + '/' + artifactId + '-' + version + ".pom");
                        if (p.exists ()) {
                            process2 (p.getCanonicalPath (), sources, level + 1);
                        }
                    }
                }
            }
        }
    }

    /*private static void process (String pom, int level) throws Exception {
        File project = new File (pom);
        Document doc = XMLUtil.parse (project);

        if (level == 0) {
            File parent = project.getParentFile ();
            File target = new File (parent, "target");
            File[] files = target.listFiles (new FileFilter () {
                @Override
                public boolean accept (File pathname) {
                    return pathname.getName ().endsWith (".jar");
                }
            });
            if (files != null) {
                for (File jar : files) {
                    Path p = Paths.get (libs.getCanonicalPath (), jar.getName ());
                    if (Files.exists (p)) {
                        Files.delete (p);
                    }
                    Files.copy (jar.toPath (), p);
                    shell.printf ("SET CP=%%CP%%;%s%n", p);
                }
            }
        }

        NodeList list = doc.getFirstChild ().getChildNodes ();
        Element dependencies = null;
        for (int i = 0; i < list.getLength (); i ++) {
            Node node = list.item (i);
            if ("dependencies".equals (node.getNodeName ())) {
                dependencies = (Element) node;
                break;
            }
        }

        if (dependencies == null) {
            return;
        }

        list = dependencies.getElementsByTagName ("dependency");
        for (int i = 0; i < list.getLength (); i ++) {
            Element e = (Element) list.item (i);

            Dependency d = new Dependency ();
            NodeList nl = e.getElementsByTagName ("optional");
            if (nl.getLength () > 0) {
                String o = nl.item (0).getTextContent ();
                d.optional = Boolean.valueOf (o);
            }
            if (d.optional) {
                return;
            }

            nl = e.getElementsByTagName ("groupId");
            if (nl.getLength () > 0) {
                d.groupId = nl.item (0).getTextContent ();
            }
            nl = e.getElementsByTagName ("artifactId");
            if (nl.getLength () > 0) {
                d.artifactId = nl.item (0).getTextContent ();
            }
            nl = e.getElementsByTagName ("version");
            if (nl.getLength () > 0) {
                d.version = nl.item (0).getTextContent ();
            }
            nl = e.getElementsByTagName ("scope");
            if (nl.getLength () > 0) {
                d.scope = nl.item (0).getTextContent ();
            }

            boolean usable = force ||
                    StringUtil.isEmpty (d.scope) ||
                    (compile && "compile".equals (d.scope));

            if (usable) {
                String path = "";
                if (!StringUtil.isEmpty (d.groupId)) {
                    path += d.groupId.replace ('.', '/');
                }
                if (!StringUtil.isEmpty (path)) {
                    path += '/';
                }
                if (!StringUtil.isEmpty (d.artifactId)) {
                    path += d.artifactId.replace ('.', '/');
                }
                if (!StringUtil.isEmpty (path)) {
                    path += '/';
                }
                if (!StringUtil.isEmpty (d.version)) {
                    path += d.version;
                }

                if (StringUtil.isEmpty (d.version)) {
                    File dir = new File (repo, path);
                    File[] files = dir.listFiles ();
                    Version max = new Version (null);
                    if (files != null) {
                        for (File file : files) {
                            Version v = new Version (file.getName ());
                            if (max.compareTo (v) < 0) {
                                max = v;
                            }
                        }
                    }
                    d.version = max.version;
                    path += d.version;
                }
                String name = d.artifactId + '-' + d.version;
                File jarFile = new File (repo, path + '/' + name + ".jar");
                Path target = Paths.get (libs.getCanonicalPath (), jarFile.getName ());
                if (!Files.exists (target) && jarFile.exists ())
                    Files.copy (jarFile.toPath (), target);

                shell.printf ("SET CP=%%CP%%;%s\\%s.jar%n", libs.getCanonicalPath (), name);


                File pomFile = new File (repo, path + '/' + name + ".pom");
                System.out.println (jarFile.getCanonicalPath ());

                if (pomFile.exists ()) {
                    process (pomFile.getCanonicalPath (), level + 1);
                }
            }
        }
    }

    private static final class Dependency implements Comparable<Dependency> {
        String groupId, artifactId, version, scope;
        boolean optional;

        @Override
        public int compareTo (Dependency o) {
            if (o == null || StringUtil.isEmpty (o.version)) {
                return 1;
            }
            if (StringUtil.isEmpty (version)) {
                return -1;
            }

            return new Version (version).compareTo (new Version (o.version));
        }
    }
*/
    private static final class Version implements Comparable<Version> {
        String version;

        Version (String version) {
            this.version = version;
        }

        @Override
        public int compareTo (Version o) {
            if (o == null) {
                return -1;
            }

            if (StringUtil.isEmpty (o.version)) {
                return 1;
            }

            if (StringUtil.isEmpty (version)) {
                return -1;
            }

            String[] a =   version.split ("[.\\-]");
            String[] b = o.version.split ("[.\\-]");
            int count = Math.min (a.length, b.length);
            for (int i = 0; i < count; i ++) {
                int na, nb;
                try {
                    na = Integer.parseInt (a[i]);
                    nb = Integer.parseInt (b[i]);
                    int code = na - nb;
                    if (code != 0) {
                        return code;
                    }
                } catch (Exception ex) {
                    int code = a[i].compareTo (b[i]);
                    if (code != 0) {
                        return code;
                    }
                }
            }

            return a.length - b.length;
        }
    }
}